###############################################################################
#                                                                             #
#    This file is part of the "ssbot" Python module.                          #
#    It is distributed under the MIT License. See LICENSE.txt for details.    #
#                                                                             #
###############################################################################

import json
import multiprocessing
import os
import os.path
import Queue
import subprocess
import sys
import threading
import time

from SimpleHTTPServer import SimpleHTTPRequestHandler
from SimpleXMLRPCServer import SimpleXMLRPCServer
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
from SocketServer import TCPServer

import ssdb
import sshtml
import ssutils


STOP_COMMAND = 1


class TryPatchListener(object):
    class RpcRequestHandler(SimpleXMLRPCRequestHandler):
        rpc_paths = ('/RPC2',)

    class Method(object):
        def __init__(self, relay_queue, response_queue):
            self.relay_queue = relay_queue
            self.response_queue = response_queue

        def try_patch(self, patch_map, patch_src):
            self.relay_queue.put(
                { 'patch_map': patch_map, 'patch_src': patch_src })
            return self.response_queue.get()

    def __init__(self, patch_port, relay_queue, response_queue):
        self.patch_port = patch_port
        self.relay_queue = relay_queue
        self.response_queue = response_queue

    def __call__(self):
        server = SimpleXMLRPCServer(
            ('', self.patch_port),
            requestHandler=TryPatchListener.RpcRequestHandler)
        server.register_instance(
            TryPatchListener.Method(self.relay_queue, self.response_queue))
        server.serve_forever()


class UiRenderer(object):
    def __init__(self, base_url, ui_port,
                 scheduler_name, scheduler_db, builder_db_map):
        self.base_url = base_url
        self.ui_port = ui_port
        self.scheduler_name = scheduler_name
        self.scheduler_db = scheduler_db
        self.builder_db_map = builder_db_map

    def __call__(self):
        def get_handler(request, client_address, server):
            return sshtml.HTTPRequestHandler(
                request, client_address, server,
                self.scheduler_name, self.scheduler_db, self.builder_db_map,
                self.base_url, self.ui_port)
        httpd = TCPServer(("", self.ui_port), get_handler)
        httpd.allow_reuse_address = True
        httpd.serve_forever()


class BuildObjectRelay(object):
    def __init__(self, builder_db, 
                 input_build_object_queue, ack_queue, builder_queue):
        self.builder_db = builder_db
        self.input_build_object_queue = input_build_object_queue
        self.ack_queue = ack_queue
        self.builder_queue = builder_queue
        self.next_build_number = builder_db.get_build_count() + 1

    def __call__(self):
        while True:
            try:
                build_object = self.input_build_object_queue.get(block=False)
            except Queue.Empty:
                time.sleep(2)
                continue
            build_object['build_number'] = self.next_build_number
            self.builder_db.add_new_build(self.next_build_number,
                                          build_object['reason'])
            self.next_build_number += 1
            self.builder_queue.put(build_object)
            self.ack_queue.put(build_object)


class CommandListener(object):
    class RpcRequestHandler(SimpleXMLRPCRequestHandler):
        rpc_paths = ('/RPC2',)

    class Method(object):
        def __init__(self, relay_queue):
            self.relay_queue = relay_queue

        def run_cmd(self, cmd):
            self.relay_queue.put(cmd)
            return 'Command sent successfully.'

    def __init__(self, cmd_port, cmd_relay_queue):
        self.cmd_port = cmd_port
        self.cmd_relay_queue = cmd_relay_queue

    def __call__(self):
        server = SimpleXMLRPCServer(
            ('localhost', self.cmd_port),
            requestHandler=CommandListener.RpcRequestHandler)
        server.register_instance(CommandListener.Method(self.cmd_relay_queue))
        server.serve_forever()


class TryScheduler(object):
    def __init__(self, name, builders,
                 base_url, patch_port, ui_port, cmd_port):
        self.name = name
        self.builders = builders
        self.base_url = base_url
        self.patch_port = patch_port
        self.ui_port = ui_port
        self.cmd_port = cmd_port
        self.db = ssdb.SchedulerDB(self.name, [b.name for b in builders])

        self.cmd_queue = multiprocessing.Queue()
        self.patch_queue = multiprocessing.Queue()
        self.patch_ack_queue = multiprocessing.Queue()

        self.build_set_count = self.db.get_build_set_count()
        self.builder_descriptors = {}
        self.builder_db_map = {}
        for b in builders:
            builder_input_queue = multiprocessing.Queue()
            builder_db = b.connect(builder_input_queue)
            relay_queue = multiprocessing.Queue()
            relay_response_queue = multiprocessing.Queue()
            relay = BuildObjectRelay(builder_db, relay_queue,
                                     relay_response_queue, builder_input_queue)
            self.builder_descriptors[b.get_name()] = {
                    'builder': b,
                    'builder_process': multiprocessing.Process(target=b),
                    'builder_db': builder_db,
                    'relay_process': multiprocessing.Process(target=relay),
                    'relay_queue': relay_queue,
                    'relay_response_queue': relay_response_queue
                }
            self.builder_db_map[b.get_name()] = builder_db

        self.cmd_listener_process = multiprocessing.Process(
            target=CommandListener(self.cmd_port, self.cmd_queue))
        self.ui_process = multiprocessing.Process(target=UiRenderer(
            self.base_url, self.ui_port, self.name, self.db,
            self.builder_db_map))
        self.try_patch_listener_process = multiprocessing.Process(
            target=TryPatchListener(patch_port,
                                    self.patch_queue,
                                    self.patch_ack_queue))

    def add_new_patch(self, patch_map, patch_src):
        build_number_map = {}
        self.build_set_count += 1
        build_object = { 
            'patch_map': patch_map,
            'reason': patch_src,
            'build_set_number': self.build_set_count
        }
        build_number_map = {}
        for name in self.builder_descriptors:
            builder_desc = self.builder_descriptors[name]
            relay_queue = builder_desc['relay_queue']
            relay_response_queue = builder_desc['relay_response_queue']
            relay_queue.put(build_object)
            response_object = relay_response_queue.get(block=True)
            build_number_map[name] = response_object['build_number']
        self.db.add_new_build_set(
            self.build_set_count, patch_src, build_number_map)
        build_set_url = (
            '%s:%d/%s/build_sets/%d' %
            (self.base_url, self.ui_port, self.name, self.build_set_count))
        user_message = ('Try job queued successfully.\n'
                        'Follow your build here: %s.\n' % build_set_url)
        return user_message

    def start(self):
        for name in self.builder_descriptors:
            self.builder_descriptors[name]['builder_process'].start()
            self.builder_descriptors[name]['relay_process'].start()
        self.cmd_listener_process.start()
        self.ui_process.start()
        self.try_patch_listener_process.start()
        while True:
            try:
                cmd = self.cmd_queue.get(block=False)
                if cmd['type'] == STOP_COMMAND:
                    self.cmd_listener_process.terminate()
                    self.ui_process.terminate()
                    self.try_patch_listener_process.terminate()
                    sys.exit(0)
                else:
                    # No other type of command is currently supported
                    pass
            except Queue.Empty:
                pass
            try:
                patch_object = self.patch_queue.get(block=False)
                message = self.add_new_patch(
                    patch_object['patch_map'], patch_object['patch_src'])
                self.patch_ack_queue.put(message)
            except Queue.Empty:
                pass
            time.sleep(2)
